MoE Distribute Dispatch V2(Full-Mesh)SIMT 内核实现分析

阅读主线 这份源码导读回答一个核心问题:面对运行时才确定的 Top-K 不规则路由,Ascend C 内核如何把 SIMT 线程分工、Full-Mesh 通信窗、URMA WQE 与接收端前缀和组合成一条完整的 MoE dispatch 流水线。建议先把握“本地打包 → 远程提交 → 完成通知 → 接收重排”四段主线,再进入函数与位域细节。
分析对象deepep_moe_dis_dispatch.h(头文件声明名 moe_distribute_dispatch_v2_full_mesh.h,4357 行,Ascend C SIMT 实现)
读者对象:零基础的算子开发初学者
阅读约定:文中的 L123 表示所附头文件的源码行号;下载后的单文件不依赖本机源码路径
SIMT 多线程MoE 专家并行 EPURMA 远程内存访问Ascend C通信算子

1. 算子概述

1.1 这个算子是做什么的?

在混合专家(MoE, Mixture of Experts)大模型中,每一层有上百个"专家"(每个专家本质上是一个 FFN 子网络),分布在不同的 AI 加速卡上。每个 token(词元)只会激活其中的 Top-K 个专家。因此必须有一个通信算子,把本卡的 token "快递"到持有目标专家的那张卡上——这就是 Dispatch(分发)算子。训练/推理完成后,还有反向的 Combine(回收)算子 把专家的输出寄回来。

卡 0(Rank 0) Token A → 专家 2,5 Token B → 专家 1,4 Token C → 专家 3,6 本卡只驻留专家 1~3 卡 1(Rank 1) 驻留专家 4~6 需要收到: A→5, C→6 ... Dispatch:把 token 发给专家所在卡 (反向即 Combine,本文不展开)
图 1-1 MoE Dispatch 算子的业务含义:按 Top-K 专家路由做跨卡分发

本文件的实现有三大特点,也是本文的主线:

特点含义对应代码位置
SIMT 实现不是传统的 SIMD(向量)风格,而是用 __simt_vf__ 线程函数 + 原子操作完成不规则路由(每个 token 去向不同)L122 simt_prepare_mapping
URMA 硬件通信用 AI Core 直接生成 WQE(工作队列元素)并"敲门铃",由 URMA 引擎执行远程写,绕过 CPU/HostL3277 simt_nw_mj_vfL4001 RingDoorbell
Full-Mesh 全互联每张卡和所有其他卡之间直接有通信窗(window),点对点读写,无需交换机式的环形/树形路由类名 FullMesh;窗口地址函数 L462 GetWindAddrByRankId
类比 把 Dispatch 想象成"快递分拣中心":token 是包裹,专家 ID 是收件地址,Top-K 意味着每个包裹要复印 K 份寄给 K 个地址。本算子就是完成"复印 + 分拣 + 发货 + 对方签收入库(按专家归档连续摆放)"的全过程。

2. SIMT 架构基础概念

2.1 什么是 SIMT?和 SIMD 有什么不同?

SIMD(单指令多数据):一条向量指令同时处理一整块数据,编程时把数据切到 UB(Unified Buffer,统一缓存)里,用 AddDataCopy 等 API 搬运和计算——适合规则的大块数据。

SIMT(单指令多线程):像 GPU 一样,一个 AI Core 内启动成百上千个线程,每个线程有独立的程序计数器、可独立分支,通过原子操作栅栏协作——适合不规则依赖数据内容的场景。Dispatch 恰恰就是典型的不规则场景:每个 token 该写到哪个槽位,只有运行时才知道。

SIMD(向量)模型 向量指令 Add 数据块 [0][1][2][3]...[N] 一次性并行处理 • 以 Buffer/UB 切分为核心 • DataCopy 显式搬运 GM↔UB • 适合规则、可预知的访问模式 SIMT(线程)模型 线程 0 线程 1 线程 2 ...线程 N 每线程独立分支:if / while / 原子操作 UB 同核共享(__ubuf__ 指针)+ 原子计数器 • 以 核数切分 + 线程数设置 为核心 • 支持直接读写 GM,无需显式搬运 • 适合不规则路由 / 数据依赖分支(本算子)
图 2-1 SIMD 与 SIMT 两种执行模型的直观对比

2.2 Ascend 上的 SIMT 编程模型(本文代码用到的全部语法)

本算子只用到了下面这一套"最小语法集",先看懂它们,后面读代码就毫无障碍:

语法 / API作用本算子示例
__simt_vf__ __aicore__标记这是一个 VF(向量函数),可在核内以多线程方式启动L122
LAUNCH_BOUND(N)声明线程数上限,N 必须是编译期常量(本算子全是 constexpr)LAUNCH_BOUND(PREPARE_THREADS_NUM),1024 线程,L92
Simt::VF_CALL<Fn>(Dim3{N}, args...)启动 VF:N 个线程并行执行 Fn(args...),调用线程阻塞等待全部完成L3721
Simt::GetThreadIdx<0>()获取线程号 tid(0 ~ N-1)L135
__ubuf__ T* / __gm__ T*地址空间标注:__ubuf__ 指向同核所有线程共享的 UB;__gm__ 指向全局内存(可跨核跨卡)L123
atomicAdd(ptr, v)原子加。本算子大量用它在 UB 上做"线程安全的计数器 + 取号机"L143
Simt::ThreadBarrier()核内线程栅栏(等价 CUDA 的 __syncthreads):所有线程到齐才继续L151
atomicOr / asc_threadfence()原子按位或(置位图)/内存栅栏(保证写序)L3360
st_dev(val, addr, 0)设备侧特殊寄存器/门铃地址写值(触发 URMA 硬件)L4262
ScalarGetSFFValue<mode>(x)位扫描:mode=1 找最低位 1(Find First one),mode=0 找最低位 0,找不到返回 -1。用于遍历 mask / 检测连续到达L1244
一个 AI Core(AIV)内部的 SIMT 视图 线程 0..1023(LAUNCH_BOUND) T0 T1 T2 每 32 线程组成 1 个 Warp(本算子按 warp 分工建 WQE) UB(共享) __ubuf__ 指针 atomicAdd 计数器 ThreadBarrier 同步 GM / 通信窗 __gm__ 直接读写 atomicAdd(跨核) st_dev 敲门铃
图 2-2 AIV 内 SIMT 编程模型:多线程 + 共享 UB + 原子操作 + GM 直访

2.3 内存层级与线程协作的三个关键机制

机制一:stride 循环(线程步进遍历)

1024 个线程要处理 BS×K 个元素(可能远多于 1024),标准写法是"每个线程从自己的 tid 出发,每次跨线程总数前进一步":

// simt_prepare_mapping 内,L139:
for (int row = tid; row < axisBS*axisK; row += PREPARE_THREADS_NUM) {
    // 线程 0 处理 row=0,1024,2048...;线程 1 处理 row=1,1025...
}

机制二:UB 原子计数器 = "取号机"

多个线程同时要往同一个专家的"槽位数组"里写数据,谁写第 0 槽、谁写第 1 槽?atomicAdd 返回旧值,天然就是一个并发取号机:

// L143-148:原子加 1 并拿到"我是第几个到达的"(pre_cnt)
int pre_cnt = atomicAdd(dstExpCnt + cur_ep_id, 1);
dstExpOffsetList[cur_ep_id*axisBS + pre_cnt] = row;  // 按取到的号入座,绝不冲突

机制三:栅栏 + 软同步

初学者常见误区 UB 内多个线程写同一地址必须原子;GM 上跨核的标志位被读之前要 DataCacheCleanAndInvalid 清缓存(否则可能永远读到旧值,见 L1689 WaitDispatch),这是本算子中反复出现的套路。

3. 算子设计思路

3.1 通信背景:Full-Mesh 与通信窗(Window)

"Full-Mesh" 指每张卡与所有 EP(Expert Parallel)域内的卡都建立了一条直连通道。发送方不需要"经过中转",直接把数据写到对方卡的接收窗口里。每张卡有一块 shmemBuffer_(共享内存窗口),并被切分成若干功能区(L462 GetWindAddrByRankId 及其周边函数):

本卡 shmem 窗口内存布局(低地址 → 高地址) ① StageDone 区 axisBS × 512B 每核一个完成标志 ② Token 发送中转区 axisBS × hCommuSize 每 token 一格(§5.4) ③ URMA 控制区 headReady / headRecord SQ(WQE 队列)/ CQ ④ 状态区 FLAG 768KB 起,每 AIV 512B flag+cnt 协议(§5.8) ⑤ 数据接收区(Window Data) 每 (源卡×本地专家) 一大块 expertPerSizeOnWin = axisMaxBS×hCommuSize GetStageDoneRegionSize() GetSendBufferAddrByTokenId() GetUrmaCtrlReserveSize() FLAG_FIELD_OFFSET=768KB GetWindAddrByRankId() 远端卡也有同样的窗口 —— 写对方 = 远程写(URMA);写自己 = 本地窗口拷贝 双缓冲:dataState_ ∈ {0,1} 在状态区/数据区内各选半区交替使用,避免上一轮残留干扰(L952-955)
图 3-1 通信窗五大功能区。发送侧写①②③,接收侧读①④⑤

3.2 多核角色分工:一条 AIV 流水线

算子运行时有 aivNum_ 个 AIV 核,代码没有让所有核干同一件事,而是把核按编号切段,各司其职(角色划分计算见 L773 SetTilingDataAndCal,调度见 L4278 Process):

AIV 核角色切分(编号从 0 开始) [0, aivUsedStage) Stage 核(发件组) 量化并写 Token → 本地 send buffer / 共享专家 [0, aivUsedRemoteWqe) URMA 核:建映射 + 生成 WQE 最后 4 核 SelfCopy:本地拷贝 1 核 RingDoorbell 敲门铃 ≤16 核 CalCumSum 计数/前缀和 全部核 LocalWindowCopy 收件整理 [aivUsedStage, aivCumSumStart) [aivCumSumStart, aivNum) Process 末尾汇合 发件组完成自己份的 token 缓存后 → PublishStageDone 置完成标志 收件组轮询 (源卡×专家) 数据块到达标志,连续化写入 expandX aivUsedCumSum = min(专家数/16, 核数/2, 16)(L815-819),剩余中间核在本轮不承担发射职责
图 3-2 AIV 多核角色分工。数字来自 L815-829 的推 导公式

3.3 生产者-消费者流水线

整个算子是一条精心设计的异步流水线:Stage 核只管"打包发货清单 + 把 token 放进发货架",URMA 核负责"填写运单(WQE)",Doorbell 核负责"按顺序按门铃通知运货车",CumSum 核负责"对账(各专家收了多少)",最后所有核作为收件方把自己专家的数据按到达顺序上架(连续化)。它们之间几乎不互相等待,靠窗口里的标志位协议衔接。

4. 核心算法流程

4.1 总体流程图

发送侧(本卡) ① AllToAllDispatch:expertIds 搬入 UB mask 过滤 / 零计算专家剪枝 / 量化 ② SendToMoeExpertNew:按 token 分核 TokenToExpert → 写 send buffer(带 flag + 三元组) ③ PublishStageDone:置 stage 完成标志 (通知 SelfCopy / Doorbell 核:发货架已备好) ④ URMASendToken(远端部分) simt_prepare_mapping 建专家→token 桶映射(原子计数) buildRemoteWqeDesc 生成运单 ⑤ simt_nw_mj:WQE 三阶段写入 SQ 环 路由分组 → 批量占头 → 写 64B WQE + 置位图 ⑥ RingDoorbell(独立 1 核并行执行) 扫 headRecord 位图 → st_dev 敲门铃 → 硬件 DMA ⑦ CalCumSum(≤16 核并行执行) 每专家 token 数 → 写远端状态区(flag+cnt) WaitDispatch 对账 → 跨核前缀和 → sendCounts/expertTokenNums 接收侧(本卡作为收件方) 硬件 URMA 写入 + 远端状态区写入 token 落入 ⑤ 数据区(每 512B 块尾 flag=1.0) ⑧ LocalWindowCopy(所有核) SetValidExpertInfo:从 sendCounts 算出自己要收哪些块 CheckDataArriveWithFlag:按块轮询到达标志 ⑨ CopyInAndOut:批量搬入 UB → 搬出 expandX 用携带的 top-k expert id 列表修正 expandIdx 三元组 ⑩ 输出 expandXOut(按专家连续排列)/ expandIdxOut expertTokenNumsOut / sendCountsOut ⑪ SelfCopyToken(发往本卡专家的部分) WaitAllStageDone 后,从 send buffer 拷入本卡窗口数据区 (与 URMA 远程发送互为补充:本卡不经硬件) 同步关系:③→⑥/⑪ 用 StageDone 标志;④ 等待 Doorbell 核 headReady;⑦↔接收侧用 flag+cnt 软同步;⑧ 轮询每块 flag
图 4-1 端到端流程总览(①-③⑦ 为发送侧动作,⑧-⑩ 为接收侧动作,二者由 URMA 硬件衔接)

4.2 阶段一:Token 本地缓存(Stage 核 → send buffer)

  1. 专家 ID 预处理ExpIdsCopyAndMaskCal):把 expertIds(BS×K 的矩阵)搬进 UB,若存在 token 掩码 / 专家掩码 / 零计算专家,用 CompareScalar + GatherMask 生成筛选掩码,无效位置填 -1。这一步保证后续"每个有效 (token, topk) 对"恰好被发送一次。
  2. 按 token 分核SendToMoeExpertNew):每个 Stage 核认领一段 token(SplitToCore 均分 + 余数前置),把每个 token 只写一次到本地 send buffer 第 tokenId 格。
  3. 打包TokenToExpertInQuant / TokenToExpert):可选量化,然后 FillTriple 在数据尾部写入三元组 (epRankId, tokenIndex, topkIndex) 以及完整的 top-k expert id 列表L1015-1028),再按 512B 块铺到 send buffer。
  4. 完工广播PublishStageDone):置自己核的 stageDone 标志 = 1.0。
🎯 设计要点 "每 token 只写一次 send buffer,之后由 URMA 核按需转发给多个远端专家",相比"每个 (token, expert) 对都量化搬运一次",把量化计算次数从 BS×K 降到 BS。携带完整 top-k 列表则是为了转发后能恢复真实的 topkIndex(见 §5.9)。

4.3 阶段二:URMA 远程发送(WQE 核)

  1. 建映射:VF simt_prepare_mapping(1024 线程)扫全部 expertIds,用 UB 原子计数把"目标专家在本核负责区间内"的 (token, 顺序) 记进桶里,并统计要发给远端的 token 总数
  2. 等绿灯:轮询 headReady 标志(L3772),确认 Doorbell 核已读完 SQ 头指针,避免并发改写竞争。
  3. 造运单:VF buildRemoteWqeDesc(128 线程 = 4 warp,每 warp 负责一个"专家×槽位段")计算每条转发的 (本地地址, 远端地址, 目标卡, 长度, 操作码) 五元组,写入 UB 上的 WQE 描述数组。
  4. 写入硬件队列:VF simt_nw_mj_vf 三阶段把运单变成硬件可消费的 64B WQE,写入各卡对应的 SQ(Send Queue)环形缓冲区。

4.4 阶段三:敲门铃(Doorbell 核)

WQE 写完只是"把信投进邮筒",还要"摇晃通知旗"硬件才开始搬运——这就是 doorbell(门铃)。由 1 个专用核(RingDoorbell)轮询每个 jetty(每卡 1 条通道)的 headRecord 位图,发现连续就绪的 WQE 后批量 st_dev(currHead, dbAddr) 通知硬件。它还负责前置工作:从 ACLSHMEM 拷贝 SQ/CQ 上下文(get_sqs_and_cqs_xb)并清缓存直写。

4.5 阶段四:计数与 CumSum(对账)

CumSum 核组(≤16 核,CalCumSum)完成"收发对账":

  1. 报数CalAndSendCnt 统计"本卡会发给每个专家多少 token",以 flag+cnt 格式写入各目标卡状态区;
  2. 对账WaitDispatch 自旋等待所有源卡报数完成(flag 全为 1),随后 GatherSumRecvCnt + GetCumSum 做跨核前缀和(并行前缀:核 i 得到核 0..i-1 的累计收件数),结果写 sendCountsOut 与 workspace;
  3. 输出专家 token 数:一个核执行 SetExpertTokenNums,对各专家跨所有源卡求和。

4.6 阶段五:接收整理(所有核 → LocalWindowCopy)

最后所有核汇合执行 LocalWindowCopy:每个核认领若干 (源卡×本地专家) 数据块,轮询块的到达标志(§5.9),到多少搬多少,把窗口里的"按源卡分块"数据重排成"按专家连续排列"的 expandXOut,同时写 expandIdxOut,最后清理标志供下一轮复用。这是典型的"工作窃取/轮询调度":数据没到就跳去看下一个块,先到先搬,最大化吞吐。

5. 关键代码逻辑解析

5.1 常量与全局布局(先记住这几个数字)

常量含义
SPLIT_BLOCK_SIZE / SPLIT_BLOCK_DATA_SIZE512 / 480 B传输的最小单元:512B 中 480B 放数据,末尾 32B 中的第一个 float 是到达标志(=1.0)
STATE_OFFSET32 B状态区最小格:float flag + int32 cnt + 冗余对齐
PREPARE_THREADS_NUM / WQE_THREADS_NUM / WARP_SIZE1024 / 128 / 32各 VF 的线程数(编译期常量,必须与 Dim3{} 一致)
MAX_WQE_CNT128一次 VF 调用最多积累的 WQE 条数(UB 描述数组容量)
headReadyFlag20260817魔数(日期戳),WQE 核与 Doorbell 核的握手暗号
BUFFER_NUM / dataState_2窗口双缓冲,两轮 dispatch 交替使用前后半区

5.2 simt_prepare_mapping:原子计数 + 桶内排序

L122-246。输入是 expertIds,输出是"我负责的每个专家收到的 token 索引列表(按 token 序号升序)"。两阶段:

// 阶段 1:扫描全部 BS×K 个槽位(stride 循环)
for (int row = tid; row < axisBS*axisK; row += PREPARE_THREADS_NUM) {
    int ep_id = *(expertIds + row);
    if (ep_id >= beginExpertId && ep_id < expertNum + beginExpertId) {
        int cur_ep_id = ep_id - beginExpertId;          // 换算成本核区间内编号
        int pre_cnt = atomicAdd(dstExpCnt + cur_ep_id, 1); // 取号:我是该专家第 pre_cnt 个 token
        dstExpOffsetList[cur_ep_id*axisBS + pre_cnt] = row; // 入桶(桶宽 axisBS)
    }
}
Simt::ThreadBarrier();   // 全员到齐才开始阶段 2(排序要读完整桶)

// 阶段 2:对每个桶元素,数一数桶里比它小的元素个数 = 它的升序位置(计数排序思想)
for (uint32_t t = tid; t < expertNum * axisBS; t += PREPARE_THREADS_NUM) {
    ...
    for (uint32_t c = 0; c < offsetNum; ++c)
        if (dstExpOffsetList[curExpertSlotBase + c] < curTokenOriginalOffset)
            sortedSlotInExpert++;
    dstExpOrderedOffsetList[curExpertSlotBase + sortedSlotInExpert] = curTokenOriginalOffset;
}
为什么要排序? 接收端要保证"同一 (源卡, 专家) 的 token 按原 token 序号连续排列",这样发送端按序填槽、接收端按序取出,双方无需再协商顺序。阶段 2 的复杂度是 O(n²)(n = 桶内元素数),这是作者用简单性换性能的选择,见 §8 改进方向。

5.3 buildRemoteWqeDesc:warp 协作生成运单

L250-299。128 线程 = 4 warp,每轮(loopIdx)每个 warp 认领一个"专家×32 槽"块:

const uint32_t tid = Simt::GetThreadIdx<0>();
const uint32_t warpId = tid / WARP_SIZE, laneId = tid % WARP_SIZE;
// 本 warp 本轮负责的 (专家, 第几批32槽)
uint32_t currBlock = (BUILD_THREADS_NUM / WARP_SIZE) * loopIdx + warpId;
uint32_t mySlot    = currBatchOfCurrExp * WARP_SIZE + laneId;  // 本线程检查的槽位
if (targetRankId != localRankId && mySlot < validOffsetCnt) {  // 只处理远端 & 有效槽
    uint32_t taskOffset = atomicAdd(finishedWqeDescNum, 1); // 取号:运单序号
    localAddrList[taskOffset]  = srcBaseAddr + tokenId * tokenCommSize;       // 源:send buffer 第 tokenId 格
    remoteWqeAddrList[taskOffset] = remoteBaseAddr + (targetExpertOnRank * axisBS + mySlot) * tokenCommSize; // 目的:远端窗口槽位
    dstRankList[taskOffset] = targetRankId;  messageLenList[taskOffset] = tokenCommSize;
}
关键映射 远端槽位地址 = 远端窗口基址 + (本地专家在对方卡上的编号 × axisMaxBS + 该专家内第 mySlot 个 token) × hCommuSize。这正是接收端 LocalWindowCopy 按块扫描的布局——发送端定槽位,接收端就能"按图索骥"。

5.4 Token 发送:512B 块布局与三元组

TokenToExpertInQuant 展示了"一个 token 如何被铺成若干 512B 块":blockCntPerToken_ = Ceil(hOutSizeAlign_, 480),每块 480B 数据 + 32B(其中 flag float 占位)。UB 内用 Copy 指令以 256B 为段、跨步 16/15 双段拷贝,把数据填入 outTensor_ 的两个半区(flagPadOffset_ 在 0/256 间乒乓),再整块 DataCopy 到 GM。

一个 token 在传输中的块布局(hCommuSize = blockCntPerToken × 512B) 块 0:数据 480B (量化后的 token 头 480 字节) flag =1.0 块 1:数据 480B flag 末块尾部区域: 三元组 (rank, tokenIdx, topkIdx) + 完整 top-k expert id 列表 接收端只需扫描每块尾部的 flag(float==1.0)即可知道该块数据已由硬件完整写入 发送端写数据 → 最后写 flag(硬件保证写序 + 接收端 DataCacheClean 保证可见性) 为什么是 480?480 = 120×4B,块数据区按 float 计 120 个;512B 整块与 WIN_ADDR_ALIGN=512 对齐,便于硬件与位图寻址
图 5-1 token 传输块布局:数据 + 尾部标志 是整条链路可靠性同步的基石

5.5 simt_nw_mj_vf:WQE 三阶段写入(性能精髓)

L3277-3407。SQ(发送队列)是每条 jetty 一个环形缓冲,多线程并发写环形队列必须解决"队列头指针 headAddr 在 GM 上、每次 atomicAdd 很贵"的问题。解法是三阶段聚合:

阶段做什么性能意义
Phase 1 路由计数每线程领一条运单,atomicAdd(dstRankWqeCount[dstRank]) 得到"它是发往该卡的第几条",映射到 SQ,并 atomicAdd(sqWqeCount[sq]) 统计每条 SQ 本批数量原子操作全部落在 UB(快)
Phase 2 批量占头每条非空 SQ 由一个线程执行一次 GM atomicAdd:sqBaseHead[sq] = atomicAdd(headAddr, count)把 GM 原子次数从 每 WQE 一次降到 每活跃 SQ 每批一次
Phase 3 写入 + 记录每线程取回自己的运单,atomicAdd(sqCursor[sq]) 在预留区间内领偏移,调 simt_write_wqe_v4 写 64B WQE;asc_threadfence() 后用 atomicOr 在 UB 位图(HeadRecordUB)中标记该槽就绪写序保证 + 位图供 Doorbell 核快速扫描
// Phase 2(L3334-3341):核心技巧
for (int sqIdx = tid; sqIdx < total_jetty; sqIdx += WQE_THREADS_NUM) {
    uint32_t count = sqWqeCount[sqIdx];
    if (count > 0U)
        sqBaseHead[sqIdx] = atomicAdd((__gm__ uint32_t *)(sqs[sqIdx].headAddr), count);
        // 一次预留 count 个连续槽位,Phase 3 在区间内自由分配
}
// Phase 3(L3344-3361):写入 + 栅栏 + 置位图
...
simt_write_wqe_v4(my_sq, my_head, ...);
asc_threadfence();                     // 确保 WQE 先于位图可见
atomicOr(&HeadRecordUB[...], 1U << bitInWord);  // 标记第 my_head % maxHeadRecordSlot 槽就绪

最后(L3365-3378)按 warp 把 UB 位图中非零 word atomicOr 合并到 GM 的 HeadRecord——Doorbell 核读的就是它。

5.6 simt_write_wqe_v4:64B WQE 位域布局

L3031-3092。WQE(Work Queue Element)是 URMA 硬件的"运单",固定 64B,两个 ulonglong4(各 32B)写入。关键位域:

字段位置含义
ownerWord0 bit31环形队列翻转位:(myHead & qpDepth)==0 ? 1 : 0,硬件据此区分"新写的"与"上一圈残留的" WQE
flag / tokenEn / rmtJettyTypeWord0 bit16-29描述符有效性、token 使能、远端 jetty 类型
opcodeWord0 bit40+本算子恒为 OP_WRITE(远程写)
tpId / sgeNumWord1传输端口 ID;SGE(分散聚合元素)个数=1
rmtObjId / rmtEidWord1、Word2-3远端 jetty 标识 / 远端端点 ID(16 字节,来自 ACLSHMEM 上下文)
rmtAddrWord10-11 (dw[5])远端目的地址(接收窗口槽位)
SGE: len / laddrdw[6] / dw[7]传输长度 hCommuSize / 本地源地址(send buffer)
版本演进 文件保留了 v2~v5 五个版本的写 WQE 函数(L2968/L3031/L3094/L3159/L3232),区别主要是寻址粒度与写法优化;当前主路径使用 v4。这是典型的"演进式原型"代码风格。

5.7 RingDoorbell:位图扫描与敲门

L4001-4275。为什么单独一个核干这事?因为 SIMT 线程无法正确地直接敲门铃(源码注释 L3381 明确说明),于是 WQE 核把就绪信息写进位图,由 Doorbell 核(Scalar 核属性)串行确认后敲:

  1. 初始化:读各 SQ 当前 head 作 prevHead(防竞争基线);set_gm_value VF 清零 headRecord 区;向各 WQE 核的 headReady 槽写魔数 20260817L4096),放行 WQE 生成。
  2. 主循环(L4165):对每张有欠账的远端卡,按 32 位 word 批量拉取位图,用 ScalarGetSFFValue<0> 统计从 prevHead 起连续置位的个数 readyCount(半 word 特判右移,L4200-4209),然后 st_dev(prevHead + wqeCount, dbAddr) 一次敲响 wqeCount 个 WQE 的门铃。
  3. 终止条件:ranksLeftToSend == 0calc_rank_token_cnt 预先算好每卡需发总数)。
🎯 批量化 门铃不是每 WQE 敲一次,而是"就绪多少敲到多少"(currHead 单调推进),把 st_dev(昂贵的设备寄存器写)次数压缩了一个数量级。

5.8 计数与跨核前缀和:flag+cnt 协议与软同步

这是全文件最精巧的核间同步设计。状态区每个 32B 格:第 0 个 int32 按 float 解释是 flag(发完置 0x3F800000=1.0f),第 1 个 int32 是 cntL1460-1463 用带掩码的 Duplicate 一次铺出这种"每 8 个元素第 1 个是 1.0"的模板)。

跨核软同步 + 并行前缀和(CalAndSendCnt → WaitDispatch → GetCumSum) 状态区(每格 32B):[flag=1.0 | cnt | ... ] 每源卡×每本地专家一格 CalAndSendCnt:本卡 CumSum 核把"我会发给你多少 token"逐格写入远端状态区 WaitDispatch(每核负责一段格子) 清 DCache → DataCopy 拉取 → ReduceSum(flag) flag 之和 == 核数 ⇒ 本段全部到齐 → 清 flag GatherSumRecvCnt + GetCumSum 各核把本段 cnt 之和 + flag=1 广播给全部 CumSum 核 每核 GatherMask 收集前序核的和再 Sum ⇒ 前缀和 前缀和结果 = sendCountsOut(单调不减的累计收件数),接收端用它把每个源块定位到 expandX 的连续目标区
图 5-2 flag+cnt 协议:flag 管"到没到",cnt 管"到多少";两轮独立的"写 1 求和清零"实现两级软同步

值得注意的细节:每次轮询前都要 DataCacheCleanAndInvalid<..., CACHELINE_OUT>L1689)——AIV 的 DCache 不与远端写自动同步,不刷缓存会死循环读旧值

5.9 到达检测与输出整理

CheckDataArriveWithFlag —— 一块一块地"验货"

L1922-2049。对某源块中 copyCnt 个 token(每 token 含 blockCntPerToken_ 个 512B 子块):

// 1) 把每个子块尾部的 flag(float) 拉到 UB:DataCopy 跨步参数 {1, 512-32B ...} 天然"跳格"采集
DataCopy(flagRecvTensor_, dataFlagGlobal[token * tokenStride], flagCopyParams);
// 2) GatherMask 抽出所有 flag → CompareScalar(flag==1.0) 得位图
GatherMask(flagGatherOutTensor_, flagRecvTensor_, flagRecvGatherMask_, ...);
CompareScalar(flagCompResultU8_, flagGatherOutTensor_, float(1), CMPMODE::EQ, compareCount);
// 3) ScalarGetSFFValue<0> 找位图中第一个 0(= 第一个未到的子块)
//    前面连续为 1 的子块数 / blockCntPerToken_ = 已完整到达的 token 数
return arriveFlagNum / blockCntPerToken_;
为什么按"前缀连续"判定? 发送端按序填槽、WQE 按序写、门铃按序敲,硬件按序 DMA,所以到达天然是前缀有序的——只统计前缀即可,一次位扫描替代逐块轮询。

WaitAndFormatOutput / CopyInAndOut —— 轮询式收件 + top-k 修正

WaitAndFormatOutput 以"(index+1)%validNum 轮询"在多个源块间跳转:哪块到得多先搬哪块;一次最多搬 maxCopyTokenCnt 个 token(受 UB 容量约束,L2107)。CopyInAndOut 整块 DataCopy 到 UB 后:

// L2076-2093:URMA 转发时三元组里的 topkIdx 被统一填成 0,
// 但 token 携带了完整 top-k expert id 列表 —— 在这里现场"验明正身"
for (uint32_t j = 0; j < axisK_; j++) {
    int32_t curExpertId = xOutInt32Tensor(topKInfoOffset + j);
    if (dstExpertId == curExpertId) {
        xOutInt32Tensor(tokenQuantAlign_ + 2) = j;   // 修正为真实 topk slot
        break;
    }
}
DataCopyPad(expandIdxGMTensor_[dstPosition * EXPAND_IDX_INFO], xOutInt32Tensor[...], ...);

搬完后若整块收完,还要把窗口里的 flag 清零(L2162-2170),配合 dataState_ 双缓冲,为下一轮 dispatch 复位现场。

5.10 Process:总调度

if ASCEND_IS_AIV {
    if (aivId_ < aivUsedStage_) {          // ── 发件组
        AllToAllDispatch();                 // 打包 token → send buffer / 共享专家直发
        PublishStageDone();                 // 完工标志
        tpipe_->Reset();
        if (aivId_ < aivUsedRemoteWqe_)  URMASendToken();  // 远端:建映射+造 WQE
        else                             SelfCopyToken();  // 本卡:等 stage 全完成直接拷
    } else if (aivId_ < aivCumSumStart_) {
        RingDoorbell();                     // ── 敲门组(1 核)
    } else {
        CalCumSum();                        // ── 对账组(≤16 核)
    }
    PipeBarrier<PIPE_ALL>();
    LocalWindowCopy();                      // ── 所有核:收件整理(本卡作为收件方)
}
双重身份 注意"发送侧"与"接收侧"是同一份代码在同一张卡上并发扮演的两个角色:本卡的 WQE/Doorbell 核在给别人发货,同时本卡的窗口正被别人的 URMA 写入,LocalWindowCopy 在收自己的快递。全 mesh 网中所有卡对称运行,这就是为什么窗口布局、flag 协议必须严格对称。

6. 内存布局速查(收发双方约定)

区域发送方视角接收方视角
① StageDone(每核 512B×axisBS)PublishStageDone 置 1.0SelfCopy/Doorbell 核 WaitAllStageDone 求和等待(L874
② Token send buffer(每 token hCommuSize)TokenToExpert 系列写入—(仅发送侧使用;SelfCopy 的源)
③ URMA 控制(headReady/headRecord/SQ/CQ)WQE 核写 SQ + 置位图Doorbell 核扫位图、敲门铃;硬件消费 SQ
④ 状态区(flag+cnt,每格 32B)CalAndSendCnt 写远端WaitDispatch / GatherSumRecvCnt 读本地
⑤ 数据区(每 源卡×本地专家 expertPerSizeOnWin)URMA 硬件按 WQE 写远端CheckDataArriveWithFlag 验货 → CopyInAndOut 出货

7. 性能优化策略盘点

#策略代码证据原理
1每 token 只量化一次,多目标复用SendToMoeExpertNew + URMA 二次转发量化计算 BS 次而非 BS×K 次,转发放到通信硬件
2原子操作分层:UB 优先,GM 批量化Phase 2 批量占头GM atomicAdd 每活跃 SQ 每批仅一次,摊薄跨核原子开销
3位图 + 位扫描代替逐项轮询HeadRecord atomicOr;ScalarGetSFFValue 扫前缀一次 64 位扫描判定 64 个子块状态;门铃批量敲
4多级软同步解耦StageDone / headReady / flag+cnt / cumsumFlag 四套独立协议各角色只在必要的点上握手,其余全异步流水
5warp 级分工buildRemoteWqeDesc 每 warp 认领一个专家槽位段warp 内 32 线程天然并行且无锁,atomicAdd 仅在 UB
6双缓冲 dataState 乒乓L952-955窗口与状态区各分两半,消除轮次间清理依赖(配合显式清 flag 兜底)
7向量化批量标志采集CheckDataArriveWithFlag 跳格 DataCopy + GatherMask一次 MTE 搬运采集多块 flag,避免标量逐个读 GM
8缓存直写/失效DataCacheCleanAndInvalidcacheWriteThrough跨核/跨卡数据可见性正确性保障(SQ 上下文还需直写穿透)
9UB 复用与 tpipe_ 反复 ResetCalCumSum 内 ResetLocalWindowCopy 内 Reset各阶段 UB 需求不同,按阶段重规划,190KB 上限内精细分配
10轮询调度 + 批量搬运WaitAndFormatOutput 主循环先到先服务,单次搬 maxCopyTokenCnt 个 token,摊薄固定开销

7.1 效果评估与代价分析

策略定量/定性收益代价与风险
每 token 只量化一次(策略 1)量化计算次数从 BS×K 降为 BS(Top-K=8 时约 8 倍收益);量化由向量核完成,转发交给 URMA 硬件与计算重叠WQE 描述符与转发路径复杂度上升;需携带 top-k id 列表并做接收侧修正
GM 原子批量化(策略 2)SQ 头指针的 GM atomicAdd 从 O(WQE 数) 降为 O(活跃 SQ 数 × 批数);一批 128 条 WQE 通常只需个位数次跨核原子三阶段逻辑引入两次核内栅栏;占头与写入之间需保证时序
位图 + 位扫描(策略 3)门铃(st_dev 设备写)次数降低约一个数量级;64 位扫描一次判定 64 个槽位,判定复杂度 O(1)/slotheadRecord 位图区域需要额外 GM 空间与清理开销(RingDoorbell 内 set_gm_value 清零)
四级软同步(策略 4)各角色仅在必要点握手,等待时间大部分被 URMA 传输与量化计算重叠无硬件栅栏兜底,协议正确性完全依赖 flag 语义与缓存清理;调试难度高
warp 级分工(策略 5)WQE 生成的原子操作全部落在 UB,延迟比 GM 原子低 1-2 个数量级;128 线程吞吐填满 128 条/批描述数组批容量 MAX_WQE_CNT=128 限制单轮批量,末批可能不满
双缓冲(策略 6)轮次间无需全局清空数据区,只需清各自的 flag,两轮 dispatch 可流水衔接窗口内存占用翻倍(数据区与状态区各分两半)
512B 块布局(§5.4)以块为粒度的到达检测,使"收多少搬多少"的增量处理成为可能有效载荷率 480/512 = 93.75%,约 6.25% 带宽用于 flag 与对齐
轮询调度收件(策略 10)消除"等最慢源块"的队头阻塞,整体收件时间 ≈ max(各源块到达时间) 而非 Σ(串行等待)轮询本身消耗标量/MTE 事务;validNum 很大时 index 遍历开销上升
总体评价 该实现的优化重心在"把不可预测的跨卡通信转化为可预测的本地流水":发送侧把所有不确定性(路由、排序、计数)收敛到 UB 原子操作与一次性前缀和,接收侧用 flag 位图做增量验收。整体是典型的以同步协议复杂度换通信吞吐的设计,其正确性高度依赖"写序 + 缓存清理 + 前缀有序"三个不变量(任何一条被打破都会表现为死等或脏数据)。

8. 潜在改进方向

  1. 桶内排序 O(n²) → O(n log n) 或 O(n)simt_prepare_mapping 阶段 2 的比较计数排序在单专家 token 数很大时代价高。可用 warp 级 bitonic 排序,或直接利用"取号即到达序"的性质改为按 (row) 排序的基数思路(源码中已保留 warp 协作版注释 L217-245,说明作者已探索过)。
  2. 轮询自旋的能效WaitDispatch、headReady 等自旋循环在高频场景会烧核(含 DCache 清理开销)。可引入带退避的轮询间隔,或硬件事件(如有)替代。
  3. 魔法数字与重复代码收敛:WQE 写入函数 5 个版本并存、大量被注释掉的调试块(如 L1951-2006)、DEBUG_ON/DEBUG_CLOCK_ON 调试宏混布,建议抽公共模板并用 constexpr 常量表管理位域。
  4. PORT_NUM/JETTY_NUM 目前为 1L106-107,多 jetty 可提升单卡聚合带宽与 WQE 并行度,三阶段路由代码已为其预留((writerId + ordinal) % jetty_per_rank),值得展开验证。
  5. URMAPollCQ 超时机制未完成L3474 处 300 万 cycle 超时仅打印即 break,可能掩盖故障;建议计入失败统计并上抛。
  6. 可移植性#if __NPU_ARCH__ == 3510 分支较多,位域打包依赖 make_ulonglong4;若要跨代移植,需把 WQE 布局抽象成版本化 trait。
  7. _recv 前缀和核数上限 16CUMSUM_MAX_CORE_NUM)在大规模专家数下每核状态段变长,轮询拉取量增大,可考虑树形归约替代"全量广播 + 局部 GatherMask"。
第二部分 算子全量流程系统性分析 覆盖 接口规范 → 生命周期 → 数学原理 → 阶段划分 → 代码映射 → 上下游交互 的完整生命周期。第一部分(§1-§8)的 SIMT 函数级分析全部保留,本部分在其之上做全局视角串联。

9. 算子接口规范(输入 / 输出)

9.1 核函数入口与 Init 参数

算子以模板类 MoeDistributeDispatchV2FullMeshL349)组织,Host 侧完成 Tiling 后调用 Init 绑定全部资源,再调用 Process 执行(每个 AIV 核各跑一次,aivId_ = GetBlockIdx())。Init 签名(L958-962)即算子的完整接口契约

__aicore__ inline void Init(
    GM_ADDR mc2Context,                       // MC2 共享上下文(含各卡 epHcclBuffer / shmem 基址)
    GM_ADDR x,                                // 输入 token 隐藏状态
    GM_ADDR expertIds,                        // Top-K 专家 ID 矩阵
    GM_ADDR scales,                           // 平滑 scale(可选)
    GM_ADDR xActiveMask,                      // token/专家 掩码(可选)
    GM_ADDR elasticInfo,                      // 弹性缩容信息(可选)
    GM_ADDR performanceInfo,                  // 性能打点输出(isPerformance 时)
    GM_ADDR expandXOut,                       // 输出:按专家连续排列的 token 数据
    GM_ADDR dynamicScalesOut,                 // 输出:动态量化 scale
    GM_ADDR expandIdxOut,                     // 输出:路由三元组
    GM_ADDR expertTokenNumsOut,               // 输出:每专家 token 数
    GM_ADDR sendCountsOut,                    // 输出:累计收件数前缀和
    GM_ADDR tpSendCountsOut,                  // 输出:tp 域计数(兼作耗时打点区)
    GM_ADDR workspaceGM, TPipe *pipe,          // 中间 workspace 与 UB 管道
    const MoeDistributeDispatchV2TilingData *tilingData);  // Host 预计算参数包
算子接口数据流:输入 → 内核 → 输出 输入(7 + 2) ① x     [BS, H] token 数据 ② expertIds [BS, K] Top-K 路由 ③ scales  [BS, scalesCol] ④ xActiveMask [BS] / [BS,K] ⑤ elasticInfo 弹性缩容(可选) ⑥ mc2Context 通信上下文 ⑦ tilingData Host 参数包 + performanceInfo / workspaceGM 内核(本文件) Init:绑定资源 / 解析 tiling Process:五角色流水线 路由 → 量化 → 缓存 → URMA → 对账前缀和 → 收件整理 每 AIV 核各执行一次 Init/Process 输出(5 个张量) ① expandXOut [Σcnt, H'] ② dynamicScalesOut 动态 scale ③ expandIdxOut [BS×K, 3] 三元组 ④ expertTokenNumsOut [int64] ⑤ sendCountsOut 累计前缀和  tpSendCountsOut tp 域计数 H' = 量化对齐后的传输宽度 Init 绑定 写出
图 9-1 算子级输入输出契约总览(形状与类型详表见 §9.2 / §9.3)

9.2 输入张量详表

张量数据类型形状说明与约束
xXInType(XType 模板;3510 架构下 fp4x2 系列自动降为 uint8 搬运,L352-366[BS, H]本卡 token 隐藏状态;DataCopyPad 按 copyInAxisH_ × sizeof(XInType) 整行搬入(L1006)
expertIdsint32[BS, K]Top-K 专家编号;值 -1、越界、被 xActiveMask 剔除或属于零计算量专家的槽位不发送(-1 填充见 L2454)
scalesfloat[BS, scalesCol]平滑 scale;IsSmoothScaleExist 时按 scaleInBytes_ 拼接到传输块尾部(L1084-1089)
xActiveMaskbool[BS](isTokenMask)或 [BS, K](isExpertMask)token 掩码只影响计数(activeMaskBsCnt_);专家掩码用 GatherMask 生成 validExpertIdsTensor_/有效索引(L2280-2365)
elasticInfoint32[4 + 2×epWorldSize]弹性缩容:[0]=isScalingDown, [1]=epWorldSize, [2]=sharedExpertRankNum, [3]=moeExpertNum, [4..]=rank 重映射表(读取逻辑 L724-741);使能后所有 toRankId 查表换算
mc2ContextMc2MoeContext*isMc2Context 时替代 HCCL 上下文;窗口地址经 aclshmem_ptr(shmemBuffer_, rank) 换算(L474-478)
tilingData结构体Host 侧 Tiling 产物,字段清单见 §9.4

9.3 输出张量详表

张量数据类型形状写出位置语义
expandXOutExpandXOutType[Σ expertTokenNum, H']CopyInAndOut L2073接收到的 token,按本地专家分组、组内按 (源卡前缀和 + 到达序) 连续排列,可直接作为专家 GEMM 输入
dynamicScalesOutuint8(承载 fp8 scale)同 expandX 行数 × scaleOutBytesL2072 CopyScalesToOut每行动态量化 scale,从传输块 scale 段抽出
expandIdxOutint32[BS×K, 3]L2095每输出行的 (srcRankId, tokenIndex, topkIndex) 三元组;topkIndex 由携带的 top-k id 列表现场还原(L2076-2093)
expertTokenNumsOutint64[本地专家数]SetExpertTokenNums L1795每本地专家收到的 token 总数;expertTokenNumsType_=0 输出累计值、=1 输出独立值(L1788-1792)
sendCountsOutint32[rscvStatusNum]L1751跨核前缀和后的累计收件数(单调不减),接收侧据此把每个源块映射到 expandX 的连续目标区间
tpSendCountsOutint32tp 域发送计数;DEBUG_CLOCK_ON 时被复用为耗时打点输出区(L4336-4351

9.4 Tiling 参数(Host 传入)

以下字段全部来自 SetTilingDataL744-770)与 Init 中的补充读取(L969-987),是 Host 侧 Tiling 阶段必须正确填写的参数面:

字段类型含义
bs / h / k / globalBsuint32本卡 batch size、隐藏维度、Top-K、全局 batch(用于 axisMaxBS_ = globalBs/epWorldSize)
epWorldSize / epRankIduint32EP 域卡数与本卡 rank
sharedExpertRankNum / sharedExpertNum / moeExpertNumuint32共享专家卡数 / 共享专家数 / MoE 专家总数;派生 moeExpertNumPerRank_ = moeExpertNum / (epWorldSize - sharedExpertRankNum)
aivNumuint32SetBlockDim 的核数,决定角色切分(§3-2)
scalesCol / scalesTypeSize / scalesCountuint32scale 列数 / 元素字节数 / 计数,合成 scaleInBytes_
isTokenMask / isExpertMask / zeroComputeExpertNumuint32/bool掩码模式与零计算量专家数(决定 ExpIdsCopyAndMaskCal 分支)
expertTokenNumsType / hasElasticInfo / isPerformanceuint32/bool输出语义开关(§9.3)
maxSizeForUbBufferuint32UB 临时缓冲上限(量化/掩码分支的 Buffer 规划依据)
totalWinSizeEp / isMc2Context / shmemBufferuint64/boolEP 通信窗总大小、上下文模式、shmem 基址(非 MC2 模式经 CheckWindowSize 校验,L976-977)

9.5 量化模式与数据类型组合

QuantMode行为代码证据
UNQUANT若 ExpandXOutType == XType 直接整块转发;类型不同则经 Cast 转换L1166、L1052-1055(3510 非量化直转 hifp8)
PERTOKEN_DYNAMIC_QUANT每 token 动态 scale,输出写入 dynamicScalesOutL1389
MX_QUANT / PERGROUP_DYNAMIC_QUANT按 256B 组计算 scale,搬入前需 Duplicate 填 0 清脏数据L1040-1042
量化使能判据 代码中统一使用 (QuantMode > UNQUANT) || (QuantMode == UNQUANT && ExpandXOutType != XType) 判定是否走 TokenToExpertInQuant(L1166 / L1261 / L1309)——即"显式量化模式隐式类型转换需求"都会触发量化路径。

10. 算子生命周期总览

10.1 五个生命周期阶段

A. Host 侧 Tiling 形状/通信域推导 SetBlockDim / 窗口校验 B. Init 初始化 绑张量 / 解析参数 清标志 / dataState 选区 C. Process 核心计算 发送链 + 计数链 + 通信链 (五角色并行流水线) URMA 远程写 + doorbell D. 收件整理 LocalWindowCopy expandX / expandIdx E. 输出复位 清 flag / 打点 等待下一轮 双缓冲 dataState 翻转:下一轮 dispatch 直接复用窗口,无需全局清空(E→B 虚线)
图 10-1 算子生命周期五阶段(A-E)。C 阶段内部并行关系详见 §12 时序图

10.2 Init 初始化详解

Init(L958-1012) 按严格顺序完成 7 件事:

  1. 饱和模式设置:3510 架构关闭浮点饱和(L964-966),避免量化 Cast 溢出钳位;
  2. 上下文选择:isMc2Context 时使用 Mc2MoeContext,否则取 HCCL 上下文并 CheckWindowSize 校验通信窗容量(L970-978);
  3. 张量绑定:为 10 个 GlobalTensor 调用 SetGlobalBuffer(L979-986);
  4. 参数解析:SetTilingDataAndCal(§10.3);
  5. 标志清理ClearStageDoneFlags(L834)把本卡窗口 StageDone 区清零,保证本轮"完工广播"从干净状态开始;
  6. 数据状态初始化SetDataStatus(L924)读取/翻转 dataState_ 双缓冲,并据此计算 shmemDataSizeOffset_ / winDataSizeOffset_(L952-955),把窗口切到本轮使用的半区;
  7. 窗口地址族初始化:计算 windowGM_ / statusDataSpaceGM_ / recvCntWorkspaceGM_ 及 DataCopy 参数(L995-1008)。

10.3 参数解析与派生量计算

SetTilingDataAndCalL773-831)是"参数 → 布局"的推导中枢,关键派生链:

派生量公式(代码语义)用途
moeExpertNumPerRank_moeExpertNum / moeExpertRankNum专家→rank 换算的分母
expertIdsCnt_axisBS × axisK路由矩阵元素总数
hAlignSize_ / hOutSizeAlign_量化对齐宽 + expandInfoSize = UB_ALIGN + Ceil(K×4, UB)×UBtoken 携带三元组 + top-k id 列表后的传输宽度(L802-806)
blockCntPerToken_ / hCommuSize_⌈hOutSizeAlign_/480⌉;× 512单 token 的传输块数与总传输字节数
expertPerSizeOnWin_axisMaxBS × hCommuSize窗口中每个 (源卡 × 本地专家) 数据块的尺寸
rscvStatusNum_共享专家卡:epWorldSize;MoE 卡:epWorldSize × moeExpertNumPerRank状态区格子总数 / sendCountsOut 长度
aivUsedCumSum_min(⌊专家总数/16⌋, ⌊aivNum/2⌋, 16, rscvStatusNum),下限 1(L815-819)CumSum 核数
aivCumSumStart_ / aivUsedStage_aivNum - aivUsedCumSum;其值 - aivUsedDoorbell_(1)角色分界(图 3-2)
sharedUsedAivNum_ / moeUsedAivNum_ / aivUsedRemoteWqe_按 (K+共享专家数) 比例切分;RemoteWqe = aivUsedStage_ - aivUsedSelfCopy_(4)发件组内二次分工
totalJettyNumepWorldSize × PORT_NUM(1) × JETTY_NUM(1)SQ/CQ 上下文条目数

11. 核心算法逻辑架构与数学原理

11.1 问题形式化

设本卡 token 为 x_i ∈ R^H(i ∈ [0,BS)),路由矩阵 E ∈ Z^{BS×K}E[i][j] = e 表示 token i 的第 j 个激活专家编号。定义映射:

σ : (i, j) ──▶ ( 目标卡 r(e), 本地专家 e_local = e % moeExpertNumPerRank, 组内槽位 s )

算子的目标是:对每个本地专家 e_local,把"全 EP 域所有发往它的 token"按确定的顺序(源卡优先、卡内按 token 序)连续拼成 expandX 的行段,且同时给出逆映射 expandIdx。形式化地,输出行位置由两级前缀和唯一确定:

dstPosition(源块 src) = sendCountsOut[src - 1]  +  该块内已收完成数

11.2 发送槽位寻址公式

发送侧的核心是"把每个 (i,j) 对号入座到远端窗口的某个 512B 槽位"。三种路径的寻址公式:

路径远端槽位地址公式代码对应
共享专家(SIMD 直发)Wind(r) + expertPerSizeOnWin × epRankId + slotIdx × hCommuSize,slotIdx = 有效 token 序(先验已知 = activeMaskBsCnt)L1160-1161
MoE(旧 SIMD 直发路径)Wind(r) + expertPerSizeOnWin × (epRankId × moeExpertNumPerRank + e_local) + dstTokenPreCnt × hCommuSizeL1256-1258
MoE(主路径:send buffer + URMA 转发)send buffer[tokenId](本地);再 remoteBase(r) + (e_local × axisBS + mySlot) × hCommuSize,其中 mySlot = 该 token 在"发往专家 e 的升序 token 序列"中的位次L3747buildRemoteWqeDesc L290-292
数学本质 所有公式都是同一个四维数组 Win[r][e_local][slot](每元素 hCommuSize 字节)的线性化寻址。发送方填 mySlot(升序保证唯一性),接收方按 (r, e_local) 整块扫描——寻址在发送端一次性确定,接收端零协商,这是全 mesh 无锁设计能够成立的前提。

11.3 接收定位与跨核前缀和

接收侧两个定位量:

跨核前缀和由 CumSum 核组协作完成(GetCumSum L1612-1668):核 k 收到全部核的 cnt 广播后,对 [0, k) 求和即得自己的全局偏移——本质是把 O(N²) 的两两交换压缩为"一轮全广播 + 一轮局部 GatherMask/Sum"。

11.4 到达判定原理

定义块级指示函数 flag(t, b) ∈ {0, 1}(token t 的第 b 个 512B 子块尾部的 float)。则:

token t 已完整到达  ⟺  ∀ b < blockCntPerToken : flag(t, b) = 1
可收 token 数  =  |{ 最大前缀 t : flag(0..t, ·) 全 1 }|  =  (连续置位 flag 总数) / blockCntPerToken

该判定的正确性依赖三个不变量:(1) 发送端"先数据后 flag"的写序(硬件 DMA 顺序 + 栅栏);(2) 接收端读前 DataCacheCleanAndInvalid 的可见性;(3) 到达的前缀有序性(由发送端升序填槽 + WQE 按序写 + 门铃按序敲共同保证)。三者任一被破坏,接收端要么死等要么收到半块脏数据——这也是 §7.1 总评中提到的协议脆弱点。

12. 处理阶段划分与执行顺序(时序图)

下图为一次 dispatch 中本卡各角色与远端卡、URMA 硬件之间的消息时序。三条泳道并行推进(发送链 / 计数链 / 通信链),仅在标注的握手点同步:

Stage 核组 WQE 核组 Doorbell 核 URMA 硬件 远端窗口 CumSum 核组 接收核(全体) 等 headReady=20260817(握手①) WQE 写入 SQ + 置 headRecord 位图 st_dev 敲门铃(批量推进 head) DMA:token 块 + flag=1.0 报数 flag+cnt → 状态区 WaitDispatch 轮询对账(软同步②) 前缀和广播 → sendCounts stageDone=1.0(握手③,经窗口①区) 轮询位图/flag 验货 → CopyInAndOut 清块 flag → 为下一轮复位 ①量化打包 token ②写 send buffer ③建桶映射 (prepare_mapping) ④生成运单 (buildRemoteWqe) ⑤扫位图 批量敲铃 ⑥按 WQE 远程写 ⑦每专家计数 ⑧收全卡 cnt 前缀和 ⑨SetValidExpertInfo (确定要收的块) ⑩写 expandX expandIdx expertTokenNums 并行性说明:发送链(蓝/橙)与计数链(绿)完全并行;接收核在 C 阶段同时扮演"发送方",在 D 阶段切换为"接收方"; 握手点仅 3 处:① headReady(WQE↔Doorbell)② WaitDispatch flag 对账(CumSum↔全卡)③ stageDone(Stage↔SelfCopy/Doorbell)
图 12-1 一次 dispatch 的角色交互时序(左→右为参与者,上→下为时间推进)

阶段执行顺序约束表

顺序约束保证机制代码位置
量化打包 → 远程转发stageDone 全员置位后 SelfCopy/URMA 才取数(WaitAllStageDone)L874L3964
prevHead 快照 → WQE 可写headReady 魔数握手(WQE 核等 Doorbell 核读完 SQ head)L3772 / L4096
WQE 写入 → 门铃asc_threadfence + headRecord 位图(先数据后置位)L3355-3360
数据到达 → 收件块尾 flag=1.0 前缀判定L1922
对账完成 → expandX 定位sendCounts 前缀和就绪后才允许写输出(CalRecvAndSetFlag 内软同步)L1729

13. 关键步骤与代码对应关系

#步骤函数 / VF行号实现机制关键词
1参数解析与派生SetTilingData / SetTilingDataAndCalL744/L773tiling → 布局常量推导链(§10.3)
2初始化InitL958上下文选择、张量绑定、清标志、dataState 选区
3窗口状态初始化SetDataStatus / ClearStageDoneFlagsL924/L834双缓冲半区计算(L952-955)
4专家 ID 预处理ExpIdsCopyAndMaskCalL2430CompareScalar + GatherMask 筛选、零计算专家剪枝
5token 本地缓存SendToMoeExpertNew / TokenToExpertInQuant / FillTripleL1279/L1031/L1015按 token 分核、512B 块、三元组 + top-k id 列表
6共享专家直发SendToSharedExpertL1140共享专家组内 rank 定位、SIMD 直写远端窗口
7完工广播PublishStageDone / WaitAllStageDoneL853/L874flag 求和轮询
8专家→token 桶映射simt_prepare_mapping (VF)L122UB 原子取号 + 计数排序
9运单生成buildRemoteWqeDesc (VF)L250warp 分工、五元组描述数组
10WQE 入队simt_nw_mj_vf / simt_write_wqe_v4L3277/L3031三阶段路由、批量占头、64B 位域
11敲门铃RingDoorbellL4001headRecord 位图扫描、st_dev 批量推进
12本卡专家拷贝SelfCopyTokenL3892WaitAllStageDone 后 UB 中转直拷
13计数与对账CalAndSendCnt / WaitDispatch / GatherSumRecvCntL1450/L1671/L1574flag+cnt 协议、DCache 失效、ReduceSum
14跨核前缀和GetCumSum / CalRecvAndSetFlag / SetExpertTokenNumsL1612/L1729/L1767全广播 + 局部 GatherMask/Sum
15收件整理LocalWindowCopy / CheckDataArriveWithFlag / CopyInAndOutL2196/L1922/L2052位图验货、轮询调度、topkIndex 修正
16复位与打点WaitDispatchClearStatus / 清块 flag / DebugClock 上抛L1559/L2162/L4336为下一轮 dataState 翻转做好准备

14. 上下游模块接口与数据交互

上下游模块架构与数据交互 上游:Host 框架 PyTorch / aclnn / Tiling 生成 tilingData、SetBlockDim 通信上下文层 MC2 Context / HCCL winContext ACLSHMEM(shmemBuffer、QP info) 本算子(Kernel) Init(§10.2)→ Process(§5.10) 窗口读写 + URMA 链路 + 前缀和 量化模块 quantInst_ 内嵌 URMA 硬件 SQ/CQ/jetty,DMA 引擎 远端卡 Kernel 对称运行(Full-Mesh) 下游:MoE 计算层 专家 GEMM(吃 expandX + expertTokenNums) Combine 算子 逆向聚合(expandIdx 逆映射) tilingData 窗口/QP 地址 WQE + 门铃 窗口互写 输出张量(§9.3) expandIdx
图 14-1 算子在系统中的位置:向上对接 Host/Tiling 与通信上下文,横向对接 URMA 硬件与远端对称 Kernel,向下服务专家计算与 Combine

接口交互协议清单

交互对象传递内容交互方式 / 同步
Host TilingtilingData 参数包(§9.4)、aivNum(SetBlockDim)、UB 上限(SetLocalMemorySize)Kernel 启动参数,一次性传入,无运行时交互
MC2 / HCCL 上下文各卡窗口基址(epHcclBuffer_ / GetHcclContext)、rank 世界信息Init 阶段只读解析(L970-978
ACLSHMEM 库shmemBuffer 基址、aclshmem_ptr 远端指针换算、UDMA QP 上下文(sqPtr/scqPtr/memPtr)设备侧 API 调用(L2524);数据可见性由 cacheWriteThrough 保证
URMA 硬件64B WQE(SQ 环)、doorbell(head 推进值)、CQ 轮询写 SQ + st_dev 敲铃异步触发;完成情况可经 URMAPollCQ 轮询 CQE(L3459
远端卡(对等 Kernel)token 数据块 + flag(⑤ 区)、flag+cnt 状态(④ 区)、headRecord(③ 区)URMA 远程写 + 双方约定的软同步协议(§5.8、§11.4)
下游专家计算expandXOut / dynamicScalesOut / expertTokenNumsOut标准张量输出,由上层调度在 dispatch 完成后消费
Combine 算子expandIdxOut 三元组(逆映射依据)、相同的窗口基础设施dispatch 输出即 Combine 输入,窗口 flag 协议对称复用
来源说明 本文档的参数名、字段、行为分支与行号引用均以本次提供的 deepep_moe_dis_dispatch.h 为准;该文件的 SHA-256 为 e2e87f115784687ea8c91b370183001ab7a17198203faa38e530263536da96fc

15. 术语表

AIV / AI Core
昇腾 AI 处理器上的向量计算核;本算子 SetBlockDim 启动的每个"核"即一个 AIV,GetBlockIdx() 得到 aivId。
SIMT / SIMD
单指令多线程 / 单指令多数据;前者线程级并行(本算子),后者向量级并行。
VF(Vector Function)
__simt_vf__ 标记的核内线程函数,通过 Simt::VF_CALL 以 N 线程启动。
LAUNCH_BOUND(N)
声明 VF 线程数的编译期常量宏,必须与 Dim3{N} 一致。
Warp / lane
32 个 SIMT 线程的执行单元;lane 为 warp 内线程号(tid%32)。
UB(Unified Buffer)
核内高速缓存,同核线程以 __ubuf__ 指针共享;容量约 190-216KB 可用。
GM(Global Memory)
全局显存(HBM),__gm__ 指针访问,跨核跨卡可见。
EP(Expert Parallel)
专家并行:把 MoE 的专家切分到多卡。
Top-K / axisK
每个 token 激活的专家数;expertIds 形状为 BS×K。
Window(通信窗)
每卡注册的一块可被其他卡远程读写的共享内存区(§3-1 图)。
URMA
AI 侧远程内存访问引擎:AI Core 写 WQE + 敲门铃即可触发跨卡 DMA,不经 Host。
WQE(Work Queue Element)
URMA 的 64B 工作描述符,含远端地址/本地地址/长度/操作码等。
SQ / CQ / jetty
发送队列 / 完成队列 / 通信通道实例;本算子每卡 1 port × 1 jetty。
Doorbell(门铃)
向硬件寄存器写"队列头推进值"以触发处理,st_dev 实现。
flag+cnt 协议
32B 状态格中 float 标志位(1.0=就绪)+ int32 计数,本算子的核心同步原语。
软同步
无硬件全局栅栏时,用"写标志 → 轮询求和 → 清零"在核间达成同步的方式。
dataState / 双缓冲
窗口数据分两半交替使用,轮次 r 用 dataState=r&1,避免读写交叉。
expandX / expandIdx
输出:按专家连续排列的 token 数据 / 每输出行的 (rank, tokenIdx, topkIdx) 三元组。
CumSum(前缀和)
sendCountsOut 是"各源卡累计收件数"的单调序列,用于把源块映射到 expandX 的连续区间。
ScalarGetSFFValue
位扫描 intrinsic:找最低位 1(<1>)或最低位 0(<0>),返回 -1 表示无。
DataCacheCleanAndInvalid
清并失效 DCache 行,强制后续读穿透到 GM——读远端更新前的必备动作。
st_dev
向设备侧(非普通内存)地址写值的指令,用于敲硬件门铃。
headReadyFlag = 20260817
WQE 核与 Doorbell 核的握手魔数(日期戳)。

16. 附录:调试手段与延伸阅读

16.1 内置维测设施

设施位置用途
DebugClock(i) + timePoint[16]L19-25L4336-4351各阶段耗时打点(cycle 差),最终写入 tpSendCountsOut 区供主机侧读取
AscendC::Simt::printf / AscendC::printf多处(已注释)设备侧打印;排查竞态时注意打印本身会改变时序(Heisenbug)
RunPosRecord(runPos)L2185把执行阶段编号写入状态区第 1 格,用于死后定位卡点阶段
RecordRankCommDurationL1801性能模式(isPerformanceFlag_)下记录每源卡通信耗时(取 max,atomicAdd int32 低 32 位写法见注释)

16.2 与本项目的关系

本头文件位于参考实现目录 ref/,本项目 ascend_deepep 的对应内核在 kernels/dispatch_udma.cpp,接口层在 csrc/ops/dispatch.cpp。对照阅读时可重点关注:send buffer 槽位映射、WQE 三阶段写入、flag+cnt 同步协议是否在自研版本中保留/演进。

16.3 推荐阅读顺序(针对初学者)

1先读 §2 建立 SIMT 心智模型,跑通"线程/原子/栅栏"三件套
2SendToMoeExpertNew + 图 5-1,理解 512B 块和 flag
3simt_prepare_mapping,体会原子取号
4读 §5.5-5.7 URMA 链路(建单 → 入队 → 敲门)
5最后读 LocalWindowCopy 收件整理,闭环整个数据流

本文档基于源码静态分析生成(2026-08-31);行号引用以本次提供的 deepep_moe_dis_dispatch.h 为准。